## pak文件头
## 每个文件头的格式为：
##顺序	字节数	含义
##1		1 byte	文件名的长度，注意，文件名不包含**\0**
##2		x byte	文件名，长度在上面已给出
##3		4 byte	对应数据块的长度。存储机制是低位编址（little endian），即处在某个数低位的数据储存在地址较小的内存地址中，例如0504 0000，其对应的真正的十六进制数应该是0000 0405，转换为十进制则是1029
##4		8 byte	文件时间
##5		1 byte	结束字符，如果是0x00则表示一个信息块的结束，如果是0x80则表示前一部分结束
class_name PakFileHeader
extends RefCounted

#region const
## 文件头记录的结尾（十六进制：0x00 十进制：0）
const RECORD_ENDING: int  = 0x00
## 文件头记录结尾的异或值（十六进制：0xF7 十进制：247）
const RECORD_ENDING_XOR: int = RECORD_ENDING ^ PakFile.SECRET_XOR_VALUE
## 文件头块的结尾（十六进制：0x80 十进制：128）
const BLOCK_ENDING: int  = 0x80
## 文件头块结尾的异或值（十六进制：0x77 十进制：119）
const BLOCK_ENDING_XOR: int = BLOCK_ENDING ^ PakFile.SECRET_XOR_VALUE
## 不包括文件名的文件头长度（14字节）
const LENGTH_WITHOUT_FILENAME: int = 1 + 4 + 8 + 1
## 文件头的最大长度（270字节）
const MAX_LENGTH: int = LENGTH_WITHOUT_FILENAME + 256
#endregion const

#region variable
## 文件名
var FileName: String = ""
## 文件大小
var FileSize: int = 0
## 文件时间字典，包含的键为：year、month、day、weekday、hour、minute 和 second。
var FileTimeDict: Dictionary = { }
## 是否为文件头块的结尾
var IsEndOfBlock: bool = false
#endregion variable

#region Get
## 获取文件名的长度（utf8编码之后的字节数）
## 返回：文件名的长度（utf8编码之后的字节数）
func GetFileNameLength() -> int:
	return FileName.to_utf8_buffer().size() if FileName else 0
	
## 获取文件头的长度（字节数）
## 返回：文件头的长度（字节数）
func GetLength() -> int:
	return GetFileNameLength() + LENGTH_WITHOUT_FILENAME
#endregion Get

#region _init
## 构造函数
## 参数：
## fileName-文件名。
## fileSize-文件大小。
## fileTimeDict-文件时间。
## isEndOfBlock-是否为文件头块的结尾。
func _init(fileName: String = "", fileSize: int = 0, fileTimeDict: Dictionary = { }, isEndOfBlock: bool = false) -> void:
	FileName = fileName
	FileSize = fileSize
	FileTimeDict = fileTimeDict
	IsEndOfBlock = isEndOfBlock
#endregion _init

#region Parse
## 解析文件头
## 参数：
## buffer-缓冲区
## start-起始位置
## 返回值：文件头对象
## 备注：如果解析失败，返回null
static func Parse(buffer: PackedByteArray, start: int = 0) -> PakFileHeader:
	# 检查缓冲区
	var bufferSize = 0 if not buffer else buffer.size()
	var pos = start if start >= 0 else bufferSize + start
	if bufferSize <= pos + LENGTH_WITHOUT_FILENAME:
		assert(false, "文件头剩余缓冲区长度不足。")
		return null
	# 文件名长度
	var fileNameLength: int = buffer.decode_s8(pos)
	if bufferSize < pos + fileNameLength + LENGTH_WITHOUT_FILENAME:
		assert(false, "文件头剩余缓冲区长度不足。")
		return null
	# 文件名
	pos += 1
	var fileNameBuffer: PackedByteArray = buffer.slice(pos, pos + fileNameLength)
	var fileName: String = fileNameBuffer.get_string_from_utf8()
	# 文件大小
	pos += fileNameLength
	var file_size_buffer: PackedByteArray = buffer.slice(pos, pos + 4)
	#todo:删除此行 file_size_buffer.reverse()
	var fileSize: int = file_size_buffer.decode_s32(0)
	# 文件时间
	pos += 4
	var fileTimeBuffer: PackedByteArray = buffer.slice(pos, pos + 8)
	#todo:计算文件时间错误（字节数组：177 180 56 198 145 1 205 1，值：129，761，689，871，103，150）
	var fileTimeDict: Dictionary = FileTime.GetDateTimeDictFromFileTimeBuffer(fileTimeBuffer)
	# 是否为文件头块的结尾
	pos += 8
	var isEndOfBlock: bool = buffer[pos] == BLOCK_ENDING
	# 返回
	return PakFileHeader.new(fileName, fileSize, fileTimeDict, isEndOfBlock)
	
## 解析文件头数组
## 参数：
## buffer-缓冲区
## start-起始位置
## 返回值：文件头数组
## 备注：如果解析失败，返回[]
static func ParseArray(buffer: PackedByteArray, start: int = PakFile.PAK_FILE_SIGN_LENGTH) -> Array[PakFileHeader]:
	var headers: Array[PakFileHeader] = []
	while true:
		var header: PakFileHeader = Parse(buffer, start)
		if not header:
			break
		headers.append(header)
		if header.IsEndOfBlock:
			break
		start += header.GetLength()
	return headers
	
## 解析文件头数组
## 参数：
## file-文件对象
## 返回值：文件头数组
## 备注：如果解析失败，返回[]
static func ParseArrayFromFile(file: FileAccess) -> Array[PakFileHeader]:
	var buffer: PackedByteArray = _getHeadersBuffer(file)
	return ParseArray(buffer, PakFile.PAK_FILE_SIGN_LENGTH)
	
## 获取文件头的缓冲区
## 参数：
## file-文件对象
## 返回值：文件头缓冲区
static func _getHeadersBuffer(file: FileAccess) -> PackedByteArray:
	var pos: int = PakFile.PAK_FILE_SIGN_LENGTH
	file.seek(pos)
	var filename_length: int
	var header_length: int
	var ending: int
	while true:
		filename_length = file.get_8() ^ PakFile.SECRET_XOR_VALUE
		header_length = filename_length + LENGTH_WITHOUT_FILENAME
		pos = pos + header_length
		file.seek(pos - 1)
		ending = file.get_8()
		if ending != RECORD_ENDING_XOR and ending != BLOCK_ENDING_XOR:
			printerr("解析文件头错误！\n错误的文件头结尾{0}。\n正确的文件头记录结尾是{1}。\n正确的文件头块结尾是{2}。".format([ ending, RECORD_ENDING_XOR, BLOCK_ENDING_XOR ]))
			break
		if ending == BLOCK_ENDING_XOR:
			break
	file.seek(0)
	var buffer: PackedByteArray = file.get_buffer(pos)
	PakFile.XOR(buffer)
	return buffer
#endregion parse

#region WriteToBuffer
## 将文件头写入到缓冲区
## 参数：
## buffer-待写入数据的缓冲区。
## start-缓冲区存储数据开始处的索引
## 返回值：写入是否成功
func WriteToBuffer(buffer: PackedByteArray, start: int = 0) -> bool:
	# 检查参数
	if not buffer or buffer.is_empty():
		return false
	var length: int = GetLength()
	if length <= 0:
		return false
	var bufferSize: int = buffer.size()
	if bufferSize < start + length:
		return false
	# 文件名长度
	var pos: int = start
	var fileNameLength: int = GetFileNameLength()
	buffer.encode_s8(pos, fileNameLength)
	# 文件名
	pos += 1
	var fileNameBuffer: PackedByteArray = FileName.to_utf8_buffer()
	assert(fileNameLength != fileNameBuffer.size(), "文件头中的文件名“{0}”的长度（{1}）与实际长度（{2}）不一致。".format([ FileName, fileNameLength, fileNameBuffer.size() ]))
	ArrayUtility.Copy(fileNameBuffer, buffer, 0, pos, fileNameLength)
	# 文件大小
	pos += fileNameLength
	var fileSizeBuffer: PackedByteArray = PackedByteArray([ 0x00, 0x00, 0x00, 0x00 ])
	fileSizeBuffer.encode_s32(0, FileSize)
	fileSizeBuffer.reverse()
	ArrayUtility.Copy(fileSizeBuffer, buffer, 0, pos, 4)
	# 文件时间
	pos += 4
	var fileTimeBuffer: PackedByteArray = FileTime.GetFileTimeBufferFromDatetimeDict(FileTimeDict)
	ArrayUtility.Copy(fileTimeBuffer, buffer, 0, pos, 8)
	# 是否为文件头块的结尾
	pos += 8
	buffer[pos] = BLOCK_ENDING if IsEndOfBlock else RECORD_ENDING
	# 返回
	return true

## 将多个文件头写入到缓冲区
## 参数：
## headers-文件头数组。
## buffer-待写入数据的缓冲区。
## start-缓冲区存储数据开始处的索引
## 返回值：是否全部写入成功。
## 备注：如果非最后的元素属性is_end_of_block值为true，会导致失败。
static func WriteHeadersToBuffer(headers: Array[PakFileHeader], buffer: PackedByteArray, start: int = 0) -> bool:
	if not headers or headers.is_empty():
		return false
	var idx: int = 0
	var maxIdx: int = headers.size() - 1
	for header: PakFileHeader in headers:
		if idx != maxIdx and header.is_end_of_block:
			return false
		if not header.write_to_buffer(buffer, start):
			return false
		start += header.length
		idx += 1
	return true
#endregion WriteToBuffer

#region ToBuffer
## 返回文件头缓冲区
func ToBuffer() -> PackedByteArray:
	var buffer: PackedByteArray = []
	buffer.resize(GetLength())
	WriteToBuffer(buffer, 0)
	return buffer

## 返回文件头缓冲区
## 参数：
## headers-文件头数组。
## 返回：返回文件头缓冲区。
## 备注：全部成功时返回缓冲区；否则返回空数组。如果非最后的元素属性is_end_of_block值为true，会导致失败。
static func HeadersToBuffer(headers: Array[PakFileHeader]) -> PackedByteArray:
	if not headers or headers.is_empty():
		return []
	# 计算缓冲区总长度
	var success: bool = true
	var totalLength: int = 0
	var maxIdx: int = headers.size() - 1
	var idx: int = 0
	for header: PakFileHeader in headers:
		if not header or header.GetLength() <= 0 or (idx != maxIdx and header.IsEndOfBlock):
			success = false
			break
		totalLength += header.GetLength()
		idx += 1
	if not success:
		return []
	# 写入缓冲区
	var buffer: PackedByteArray = []
	buffer.resize(totalLength)
	var start: int = 0
	for header: PakFileHeader in headers:
		success = header.WriteToBuffer(buffer, start)
		if not success:
			break
		start += header.GetLength()
	if not success:
		return []
	else:
		return buffer
#endregion ToBuffer

#region _to_string
## 返回字符串
func _to_string() -> String:
	return "文件名的长度：{0}\n文件名：{1}\n文件大小：{2}\n文件时间：{3}\n是否为文件头块的结尾：{4}".format([ GetFileNameLength(), FileName, FileSize, FileTimeDict, IsEndOfBlock ])
#endregion _to_string
